Day 7: Some Assembly Required ---

This year, Santa brought little Bobby Tables a set of wires and bitwise logic gates! Unfortunately, little Bobby is a little under the recommended age range, and he needs help assembling the circuit.

Each wire has an identifier (some lowercase letters) and can carry a 16-bit signal (a number from 0 to 65535). A signal is provided to each wire by a gate, another wire, or some specific value. Each wire can only get a signal from one source, but can provide its signal to multiple destinations. A gate provides no signal until all of its inputs have a signal.

The included instructions booklet describe how to connect the parts together: x AND y -> z means to connect wires x and y to an AND gate, and then connect its output to wire z.

For example:

123 -> x means that the signal 123 is provided to wire x.
x AND y -> z means that the bitwise AND of wire x and wire y is provided to wire z.
p LSHIFT 2 -> q means that the value from wire p is left-shifted by 2 and then provided to wire q.
NOT e -> f means that the bitwise complement of the value from wire e is provided to wire f.
Other possible gates include OR (bitwise OR) and RSHIFT (right-shift). If, for some reason, you'd like to emulate the circuit instead, almost all programming languages (for example, C, JavaScript, or Python) provide operators for these gates.

For example, here is a simple circuit:

123 -> x
456 -> y
x AND y -> d
x OR y -> e
x LSHIFT 2 -> f
y RSHIFT 2 -> g
NOT x -> h
NOT y -> i
After it is run, these are the signals on the wires:

d: 72
e: 507
f: 492
g: 114
h: 65412
i: 65079
x: 123
y: 456
In little Bobby's kit's instructions booklet (provided as your puzzle input), what signal is ultimately provided to wire a?

In [21]:
import re

_LOGIC_OP = re.compile("([a-z0-9]+) (AND|OR) ([a-z0-9]+) -> ([a-z0-9]+)")
_SHIFT_OP = re.compile("([a-z0-9]+) (LSHIFT|RSHIFT) ([0-9]+) -> ([a-z0-9]+)")
_NOT_OP = re.compile("NOT ([a-z0-9]+) -> ([a-z0-9]+)")
_ASSIGN_OP = re.compile("([0-9]+) -> ([a-z0-9]+)")
_CONNECT_OP = re.compile("([a-z]+) -> ([a-z0-9]+)")

_OP_FUNC = {
    'AND': lambda x, y: value(x) & value(y),
    'OR': lambda x, y: value(x) | value(y),
    'LSHIFT': lambda x, y: value(x) << y,
    'RSHIFT': lambda x, y: value(x) >> y,
    'NOT': lambda x: ~value(x)
}

# Extract a value from a function
def value(x):
    if callable(x):
        return value(x())
    return x & 0xffff


# Extract wire
def get_wire(p_ins, wire):

    if wire in p_ins:
        v = p_ins[wire]
        while callable(v):
            v = v()
            p_ins[wire] = v
        return v

    return int(wire)

# Functions to encapsulate values
def _logic_op(p_ins, op, wire_left, wire_right):
    return lambda: _OP_FUNC[op](lambda: get_wire(p_ins, wire_left), get_wire(p_ins, wire_right))


def _shift_op(p_ins, op, shift, wire_left):
    return lambda: _OP_FUNC[op](lambda: get_wire(p_ins, wire_left), int(shift))


def _not_op(p_ins, wire_left):
    return lambda: _OP_FUNC['NOT'](lambda: get_wire(p_ins, wire_left))


def _connect_op(p_ins, wire_left):
    return lambda: p_ins[wire_left]


# Parse and load all the instructions
def load_instructions(instructions):

    p_ins = {}
    for i in instructions:

        # Logic operation
        m = _LOGIC_OP.match(i)
        if m:
            wire_left, op, wire_right, wire = m.groups()
            p_ins[wire] = _logic_op(p_ins, op, wire_left, wire_right)
            continue

        # Shift operation
        m = _SHIFT_OP.match(i)
        if m:
            wire_left, op, shift, wire = m.groups()
            p_ins[wire] = _shift_op(p_ins, op, shift, wire_left)
            continue

        # Not operation
        m = _NOT_OP.match(i)
        if m:
            wire_left, wire = m.groups()
            p_ins[wire] = _not_op(p_ins, wire_left)
            continue

        # Value connected to a wire
        m = _ASSIGN_OP.match(i)
        if m:
            value, wire = m.groups()
            p_ins[wire] = int(value)
            continue

        # Two wires connected
        m = _CONNECT_OP.match(i)
        if m:
            wire_left, wire = m.groups()
            p_ins[wire] = _connect_op(p_ins, wire_left)
            continue

    return p_ins

In [24]:
%%time
# Test example
ins = [
    "123 -> x",
    "456 -> y",
    "x AND y -> d",
    "x OR y -> e",
    "x LSHIFT 2 -> f",
    "y RSHIFT 2 -> g",
    "NOT x -> h",
    "NOT y -> i"
]

ains = load_instructions(ins)
print("d: 72 = ", value(ains['d']))
print("e: 507 = ", value(ains['e']))
print("f: 492 = ", value(ains['f']))
print("g: 114 = ", value(ains['g']))
print("h: 65412 = ", value(ains['h']))
print("i: 65079 = ", value(ains['i']))
print("x: 123 = ", value(ains['x']))
print("y: 456 = ", value(ains['y']))


d: 72 =  72
e: 507 =  507
f: 492 =  492
g: 114 =  114
h: 65412 =  65412
i: 65079 =  65079
x: 123 =  123
y: 456 =  456
CPU times: user 374 µs, sys: 0 ns, total: 374 µs
Wall time: 384 µs

In [26]:
%%time

# Load input
with open('day7/input.txt', 'rt') as fd:
    ains = load_instructions(fd)
    
print("a = ", value(ains['a']))


a =  46065
CPU times: user 12.9 ms, sys: 3 µs, total: 12.9 ms
Wall time: 13 ms

Part Two

Now, take the signal you got on wire a, override wire b to that signal, and reset the other wires (including wire a). What new signal is ultimately provided to wire a?

In [28]:
# Load input
with open('day7/input.txt', 'rt') as fd:
    ains = load_instructions(fd.readlines() + ['46065 -> b'])
    
print("a = ", value(ains['a']))


a =  14134